package com.sos.flex
{
	import com.sos.Application;
	import com.sos.logging.Logger;
	import com.sos.utils.JsonUtils;
	
	import flash.events.EventDispatcher;
	
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;
	import mx.utils.ObjectUtil;
	import com.sos.comm.CallError;
	import com.sos.comm.CallErrorEvent;
	import com.sos.comm.CallErrorType;
	import com.sos.comm.CallEvent;
	import com.sos.comm.CallFactory;
	import com.sos.comm.CallFailEvent;
	import com.sos.comm.CallFailType;
	import com.sos.comm.CallStatus;
	import com.sos.comm.CallSuccessEvent;

	public class FlexCall extends EventDispatcher
	{
		
		// -------------------------------------------------------------------------------
		// CONSTANTS
		// -------------------------------------------------------------------------------	
		
		// Result values
		protected static const		RESULT_SUCCESS:int						=  0;
		protected static const		RESULT_ERROR:int						= -1;
		protected static const		RESULT_SERVER_EXCEPTION:int				= -2;
		
		// JSON property definition
		protected static const		JSON_PROPERTY_RESULT:String				= "result";
		protected static const		JSON_PROPERTY_DATA:String				= "data";
		protected static const		JSON_PROPERTY_ERROR:String				= "error";
		protected static const		JSON_PROPERTY_ERROR_CODE:String			= "code";
		protected static const		JSON_PROPERTY_ERROR_MESSAGE:String		= "message";
		
		// Logging
		public static const			LOG_MARKER:String						= "CALL > ";
		public static const			DEBUG_KEY:String						= "comm.call";
		
		// -------------------------------------------------------------------------------
		// STATIC
		// -------------------------------------------------------------------------------
		
		// Global handlers flag
		private static var _enableGlobalCallFailedHandling:Boolean			= true;
		private static var _enableGlobalErrorHandling:Boolean 				= true;
		
		// Global handler for all OldCall instances, invoked each time CallFailedEvent is dispatched 
		private static var _globalCallFailedHandler:Function;
		// Global handler for all OldCall instances, invoked each time CallInErrorEvent is dispatched
		private static var _globalCallErrorHandler:Function;
		
		// -------------------------------------------------------------------------------
		// FIELDS
		// -------------------------------------------------------------------------------
		
		private var _service:HTTPService;
		private var _url:String;
		private var _params:Object;
		private var _data:Object;
		private var _error:CallError;
		private var _status:CallStatus;
		private var _useAutoResultHandling:Boolean;
		
		// -------------------------------------------------------------------------------
		// CONSTRUCTOR
		// -------------------------------------------------------------------------------
		
		public function FlexCall(url:String, resultHandling:Boolean = false, params:Object = null, timeout:int = CallFactory.TIMEOUT)
		{
			// Initialization
			_url = url; // relative
			_params = params;
			_useAutoResultHandling = resultHandling;
			_status = CallStatus.READY;
			// Create new HTTPService
			var httpService:HTTPService = new HTTPService();
			httpService.url				= Application.instance.config.callsUrl + url;
			httpService.method			= "GET";
			httpService.requestTimeout	= timeout;
			httpService.resultFormat	= HTTPService.RESULT_FORMAT_TEXT;
			// Event listeners
			httpService.addEventListener(ResultEvent.RESULT, serviceResultHandler);
			httpService.addEventListener(FaultEvent.FAULT,   serviceFaultHandler);
		}
		
		// -------------------------------------------------------------------------------
		// PROPERTIES
		// -------------------------------------------------------------------------------
		
		public function get url():String
		{
			return _url;
		}
		
		public function set url(value:String):void
		{
			_url = value;
		}
		
		public function get params():Object
		{
			return _params;
		}
		
		public function set params(value:Object):void
		{
			_params = value;
		}
		
		public function get data():Object
		{
			return _data;
		}
		
		public function get error():CallError
		{
			return _error;
		}
		
		public function get hasParams():Boolean
		{
			return _params != null;
		}
		
		public function get hasData():Boolean
		{
			return _data != null;
		}
		
		public function get useAutoResultHandling():Boolean
		{
			return _useAutoResultHandling;
		}
		
		public function set useAutoResultHandling(value:Boolean):void
		{
			_useAutoResultHandling = value;
		}
		
		[Bindable(event="call_status_changed")]
		public function get status():CallStatus
		{
			return _status;
		}

		public function get executing():Boolean
		{
			return	_status == CallStatus.EXECUTING;
		}		
		
		public function get executed():Boolean
		{
			return	_status == CallStatus.EXECUTED_WITH_SUCCESS ||
					_status == CallStatus.EXECUTED_WITH_ERROR;
		}
		
		public function get succeeded():Boolean
		{
			return	_status == CallStatus.EXECUTED_WITH_SUCCESS;
		}
		
		public function get failed():Boolean
		{
			return	_status == CallStatus.FAILED;
		}
		
		
		// -------------------------------------------------------------------------------
		// PRIVATE METHODS
		// -------------------------------------------------------------------------------
		
		protected function parseResponce(raw:Object):void
		{
			// Parsing error vars
			var formatError:Boolean = false;
			var formatErrorMessage:String = "";
			// Rasult value
			var result:int = RESULT_SERVER_EXCEPTION;
			var error:CallError = null;
			// Check for responce result
			if (raw.hasOwnProperty(FlexCall.JSON_PROPERTY_RESULT))
			{
				// Get result value
				result = int(raw[FlexCall.JSON_PROPERTY_RESULT]);
				// Check the result type
				switch(result)
				{
					case FlexCall.RESULT_SUCCESS:
					{
						var data:Object = null;
						// Search for data
						if (raw.hasOwnProperty(FlexCall.JSON_PROPERTY_DATA))
						{
							data = raw[FlexCall.JSON_PROPERTY_DATA];
						}
						// OldCall > SUCCEDED
						callSucceeded(data);
						break;
					}
						
					case FlexCall.RESULT_ERROR:
					{
						// Check for error structure
						if (raw.hasOwnProperty(FlexCall.JSON_PROPERTY_ERROR) && 
							raw[FlexCall.JSON_PROPERTY_ERROR].hasOwnProperty(FlexCall.JSON_PROPERTY_ERROR_CODE) && 
							raw[FlexCall.JSON_PROPERTY_ERROR].hasOwnProperty(FlexCall.JSON_PROPERTY_ERROR_MESSAGE))
						{
							// Creates the call error
							error = new CallError(CallErrorType.GENERIC, 
												  int(raw[FlexCall.JSON_PROPERTY_ERROR][FlexCall.JSON_PROPERTY_ERROR_CODE]),
												  String(raw[FlexCall.JSON_PROPERTY_ERROR][FlexCall.JSON_PROPERTY_ERROR_MESSAGE]));
							// OldCall > ERROR
							callError(error);
						}
						else
						{
							// ERROR > 'error' structure missing or not well formed
							formatError = true;
							formatErrorMessage = "'" + FlexCall.JSON_PROPERTY_ERROR + "' structure missing or not well formed";
						}
						break;
					}
						
					case FlexCall.RESULT_SERVER_EXCEPTION:
					{
						// Check for error structure
						if (raw.hasOwnProperty(FlexCall.JSON_PROPERTY_ERROR) && 
							raw[FlexCall.JSON_PROPERTY_ERROR].hasOwnProperty(FlexCall.JSON_PROPERTY_ERROR_CODE) && 
							raw[FlexCall.JSON_PROPERTY_ERROR].hasOwnProperty(FlexCall.JSON_PROPERTY_ERROR_MESSAGE))
						{
							// Creates the call error
							error = new CallError(CallErrorType.SERVER_EXCEPTION, 
												  int(raw[FlexCall.JSON_PROPERTY_ERROR][FlexCall.JSON_PROPERTY_ERROR_CODE]),
												  String(raw[FlexCall.JSON_PROPERTY_ERROR][FlexCall.JSON_PROPERTY_ERROR_MESSAGE]));
							// OldCall > ERROR
							callError(error);
						}
						else
						{
							// ERROR > 'error' structure missing or not well formed
							formatError = true;
							formatErrorMessage = "'" + FlexCall.JSON_PROPERTY_ERROR + "' structure missing or not well formed";
						}
						break;
					}
						
					default:
					{
						// ERROR > 'result' field value unknown
						formatError = true;
						formatErrorMessage = "'" + FlexCall.JSON_PROPERTY_RESULT + "' field value (" + result + ") is unknown";
						break;
					}
				}
			}
			else
			{
				// ERROR > 'result' field missing
				formatError = true;
				formatErrorMessage = "'" + FlexCall.JSON_PROPERTY_RESULT + "' field missing";
			}
				
			
			// Checks if any format error happens
			if (formatError)
			{
				// OldCall > FAILED: response format is wrong
				callFailed(CallFailType.RESPONSE_ERROR, formatErrorMessage);
			}
			
		}
		
		protected function setStatus(newStatus:CallStatus):void
		{
			_status = newStatus;
			// dispatch the status change
			dispatchStatusChangedEvent();
		}
		
		// HTTPService event handlers
		protected function serviceResultHandler(e:ResultEvent):void
		{
			// DEBUG > Received result from server
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "OldCall.serviceResultHandler() ' {0} result:  {1}", _url , String(e.result));
			// Common JSON responce parsing
			var raw:Object = null;
			// Decoding flag
			var parsing:Boolean = false;
			// Safe exec
			try
			{
				try
				{
					// JSON Decoding
					raw = JsonUtils.decode(String(e.result));
					// Decoding succeded
					parsing = true;
				}
				catch (error:Error)
				{
					// JSON message
					var msg:String = "JSON parsing error: " + error.message + " - data: " + String(e.result); 
					// OldCall > FAILED
					callFailed(CallFailType.JSON_ERROR, msg, error);
				}
				// Check parsing result
				if (parsing)
				{
					// Verify the handling
					if (_useAutoResultHandling)
					{
						parseResponce(raw);
					}
					else
					{
						// OldCall > SUCCEDED: without any data result parsing
						callSucceeded(raw);
					}
				}
			}
			// Unexpected exception
			catch (exc:Error)
			{
				callFailed(CallFailType.EXCEPTION, "Unexpected error in serviceResultHandler(): " + exc.message, exc);
			}
		}
		
		protected function serviceFaultHandler(e:FaultEvent):void
		{
			// DEBUG > Received FAULT from server
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "OldCall.serviceFaultHandler() '{0}'", _url);
			// Safe exec
			try
			{
				// Dump fault object into a String
				var faultDump:String = "";
				faultDump += "\r\n";
				faultDump += "======================================================================"	+ "\r\n";
				faultDump += " CALL				: " + _service.url										+ "\r\n"; 
				faultDump += " PARAMS			: " + JsonUtils.encode(_params)							+ "\r\n";
				faultDump += " STATUS CODE		: " + e.statusCode 										+ "\r\n";
				faultDump += " DETAILS" 																+ "\r\n";
				faultDump += " - errorID		: " + e.fault.errorID 									+ "\r\n";
				faultDump += " - name			: " + e.fault.name 										+ "\r\n";
				faultDump += " - message		: " + e.fault.message 									+ "\r\n";
				faultDump += " - faultCode		: " + e.fault.faultCode 								+ "\r\n";
				faultDump += " - faultString	: " + e.fault.faultString 								+ "\r\n";
				faultDump += " - faultDetail	: " + e.fault.faultDetail 								+ "\r\n";
				faultDump += " - rootCause		: " + e.fault.rootCause 								+ "\r\n";
				faultDump += "======================================================================"	+ "\r\n";
				faultDump += "\r\n";
				// CALL > Failed
				callFailed(CallFailType.HTTP_ERROR, faultDump, null);
			}
			// Unexpected exception
			catch (exc:Error)
			{
				callFailed(CallFailType.EXCEPTION, "Unexpected error in serviceFaultHandler(): " + exc.message, exc);
			}
		}

		// OldCall internal handlers

		protected function callSucceeded(data:Object):void
		{
			// Save the data returned
			_data = data;
			// STATUS > EXECUTED_WITH_SUCCESS
			setStatus(CallStatus.EXECUTED_WITH_SUCCESS);
			// Event dispatch
			dispatchEvent(new CallSuccessEvent(this));
		}
		
		protected function callError(error:CallError):void
		{
			// Saves the error
			_error = error;
			// OldCall > ERROR
			setStatus(CallStatus.EXECUTED_WITH_ERROR);
			// Crete fail event
			var errorEvent:CallErrorEvent = new CallErrorEvent(this);
			// OldCall generic handling
			FlexCall.globalCallError(errorEvent);
			// Event dispatch
			dispatchEvent(errorEvent);
		}
		
		protected function callFailed(type:CallFailType, message:String = null, error:Error = null):void
		{
			// Create an CallError to store fail informations
			_error = new CallError(CallErrorType.CALL_FAILED, type.id, message);
			// STATUS > FAILED
			setStatus(CallStatus.FAILED);
			// Crete fail event
			var failEvent:CallFailEvent = new CallFailEvent(this, type, message, error);
			// OldCall generic handling
			FlexCall.globalCallFailed(failEvent);
			// Event dispatch
			dispatchEvent(failEvent);
		}

		// Dump and encoding utils
		
		protected function urlEncodeParams(urlEscape:Boolean = false):String
		{
			var dump:String = "";
			for (var p:String in _params)
			{
				if (dump != "") dump += "&";
				if (_params[p] is Array)
				{
					var a:Array = _params[p] as Array;
					dump += p + "=";
					for (var i:int = 0; i<a.length; i++)
					{
						if (i > 0) dump += ",";
						dump += urlEscape ? escape( a[i].toString() ) : a[i].toString();
					}
				}
				else
				{
					dump += p + "=" + ( urlEscape ? escape( _params[p].toString() ) : _params[p].toString() );
				}
			}
			return dump;
		}
		
		// Event dispatchers
		
		protected function dispatchStatusChangedEvent():void
		{
			dispatchEvent(new CallEvent(CallEvent.STATUS_CHANGED, this));
		}
		
		// -------------------------------------------------------------------------------
		// PUBLIC METHODS
		// -------------------------------------------------------------------------------
		
		/**
		 * Executes the call to the server 
		 */		
		public function execute():void
		{
			// Status check
			if (_status != CallStatus.READY)
				throw new Error("A OldCall is not ready to be executed! OldCall clear before to execute it again or cancel the current execution");
			// Send the HTTP request
			try
			{
				// DEBUG > Log URL and params before execute the call
				if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "OldCall.execute() '{0}' params: {1}", toUrl() ,JsonUtils.encode(_params));
				// Sends the call
				_service.send(_params);
				// Status EXECUTING
				setStatus(CallStatus.EXECUTING);
			}
			catch (e:Error)
			{
				// OldCall > FAILED
				callFailed(CallFailType.REQUEST_ERROR, "OldCall.execute() in error: " + e.message, e);
			}
		}
		
		/**
		 * Cancel the current call execution 
		 */		
		public function cancel():void
		{
			// Status check
			// if (_status != CallStatus.EXECUTING) throw new Error("OldCall is not executing the request, cannot be cancelled!");
			if (_status == CallStatus.EXECUTING)
			{
				// Cancels the call
				_service.cancel();
			}
			// Clear the call
			clear();
		}
		
		/**
		 * Clears the current call, removing receved data, errors and setting the status to READY
		 */		
		public function clear():void
		{
			_data = null;
			_error = null;
			setStatus(CallStatus.READY);
		}
	
		public function equals(call:FlexCall):Boolean
		{
			return	_url == call.url &&
					ObjectUtil.compare(_params, call.params) == 0;                                                                                                                                       
		}
		
		public function toUrl(urlEscape:Boolean = false):String
		{
			var dump:String = "";
			try
			{
				// dump += CallFactory.BASE_URL +_url;
				dump += Application.instance.config.callsUrl +_url;
				dump += "?";
				dump += urlEncodeParams(urlEscape);
			} 
			catch(error:Error) 
			{
				dump += Application.instance.config.callsUrl + _url;
			}
			return dump;
		}
		
		override public function toString():String
		{
			return "OldCall '" + _url + "' (status: " + _status.name + ", params: " + JsonUtils.encode(_params) + ", error: " + JsonUtils.encode(_error) + ")";
		}
		
		
		// -------------------------------------------------------------------------------
		// STATIC METHODS
		// -------------------------------------------------------------------------------
		
		private static function globalCallFailed(event:CallFailEvent):void
		{
			if (!_enableGlobalCallFailedHandling) return;
			if (FlexCall.hasGlobalCallFailedHandler) _globalCallFailedHandler(event);
		}
		
		private static function globalCallError(event:CallErrorEvent):void
		{
			if (!_enableGlobalErrorHandling) return;
			if (FlexCall.hasGlobalCallErrorHandler) _globalCallErrorHandler(event);
		}
		
		public static function setGlobalCallFailedHandler(handler:Function):void
		{
			if (_globalCallFailedHandler != null) throw new Error("Global handler for CallFailedEvent already set");
			_globalCallFailedHandler = handler;
		}
		
		public static function setGlobalCallErrorHandler(handler:Function):void
		{
			if (_globalCallErrorHandler != null) throw new Error("Global handler for GenericErrorEvent already set");
			_globalCallErrorHandler = handler;			
		}

		public static function get hasGlobalCallFailedHandler():Boolean
		{
			return _globalCallFailedHandler != null;
		}
		
		public static function get hasGlobalCallErrorHandler():Boolean
		{
			return _globalCallErrorHandler != null;
		}
		
		public static function get enableGlobalCallFailedHandling():Boolean
		{
			return _enableGlobalCallFailedHandling;
		}
		
		public static function set enableGlobalCallFailedHandling(value:Boolean):void
		{
			_enableGlobalCallFailedHandling = value;
		}
		
		public static function get enableGlobalCallErrorHandling():Boolean
		{
			return _enableGlobalErrorHandling;
		}
		
		public static function set enableGlobalCallErrorHandling(value:Boolean):void
		{
			_enableGlobalErrorHandling = value;
		}
		
	}
}